/**
 * @file
 * OpenCV.js官方文档 https://docs.opencv.org/4.5.5/d0/d84/tutorial_js_usage.html
 * OpenCV.js 官方文档  cv.resize, cv.warpAffine, cv.getAffineTransform and cv.warpPerspective https://docs.opencv.org/4.5.5/dd/d52/tutorial_js_geometric_transformations.html
 */

/**
 * @typedef {Object} Loc
 * @typedef {number} x
 * @typedef {number} y
 */

/**
 * @description 
 * @typedef {Object}            size              尺寸              
 * @property {number}           w                 宽 
 * @property {number}           h                 高 
 */

/**
 * @description 校验状态
 * @constant 
 * @readonly
 * @enum {number}
*/
const CHECK_STATE = {
    /** 初始状态 */
    INIT: 0,
    /** 正确 */
    TRUE: 1,
    /** 错误 */
    FALSE: 2,
};

/**
 * @typedef {Object}        scaleSearchDataType   scaleSearch函数参数data 数据类型
 * @property { number } [similarity=100] 相似度，默认值100，1-100， 值越大越相似
 * @property { [size,size] } range 缩放范围及缩放趋势，缩放的模板图片，注意要等比缩放range[0] = n * range[1]
 * @property { {w: number} | {h: number} } step 缩放步长，必须为正数，range[0]->range[1]控制变化趋势；只需传一个值，另一个等比计算
 * @property { cv.Mat } [drawSrc=null] 调试用，绘制所有满足相似度条件的所在区域选框
 */

/**
 * @description 
 * @typedef {Object}    scaleSearchResultType           scaleSearch函数返回数据类型
 * @property {number}   iDiffNum_res                iDiffNum 差异度
 * @property {number}   similarity_res              similarity_res 相似度
 * @property {number}   scale_times                 结果的缩放次序（从0开始）
 * @property {number}   size_res                    成功时匹配对应尺寸
 * @property {Loc}      match_rect_data_loc         匹配区域Loc,给_Rect方法使用
 * @property {cv.Point} match_rect_data_point       匹配区域Point,给_Rect方法使用
 */

/**
 * @global
 * @description opencv使用方法封装
 */
var $mocvf = {
    scaleSearch,
    pHash,
    calcRect,
    bwImage,
    mmCompare,
};

Object.defineProperty(window, '$mocvf', $mocvf);// $mocvf 一定要用 var定义，不能用let,不然let有块级作用域，当不了全局变量
// console.log(window.$mocvf);

// Object.defineProperty(globalThis, 'CHECK_STATE', CHECK_STATE);

/** @link window, globalThis, self区别 https://segmentfault.com/a/1190000021472711 */
// console.log("全局变量", window, globalThis, self);

/**
 * @function
 * @param { cv.Mat } src 待匹配图片，cv.CV_8UC4，建议先二值化
 * @param { cv.Mat } templ 模板图片，cv.CV_8UC4，建议先二值化
 * @param { cv.Mat } dst 匹配结果
 * @param { cv.Mat } [mask] 掩码
 * @param { scaleSearchDataType} [data = {}] 携带参数，默认值为{}
 * 
 * @returns { scaleSearchResultType } 
 * 
 * 
 * @memberof window.$mocvf
 * @description 缩放模板图片并进行模板匹配，cv.matchTemplate 循环 指定范围，获取结果。模板图片目前是贴边裁剪，没有留白了，所以只需要先放大到跟待匹配图片一致，再缩小匹配就行了。原先在5_runeTool.js文件中，该函数与项目业务逻辑耦合，现解耦
 * 
 * @todo scale_matched_rune_sus_i_1st 这个值用它会不会有问题，比如第一次成功缩放匹配的那个rune其实是误识别的rune;换不同分辨率截图时这个问题已经出现了（主要是模板图片放大到最大也没有实际截图中的大），调整start_i,end_i,增大缩放范围，已匹配时长的代价换取匹配精度
 * @todo 精度缩放调整，在第一次找到最好的模板匹配时，以这为起始值，左右缩放（微调），默认缩放比率 0.05，注意不要随意改0.05，改它的话要考虑循环次数
 * @todo 考虑new cv.Rect(object,cv.Point)得到的rect ，其width,height 为undefined的问题 用_Rect是否有隐患
 * @todo 研究pHash函数，以及考虑它是否在scaleSearch中使用有没有错误的可能 iDiffNum有啥用
 * @todo 研究dst.convertTo(dst, cv.CV_8UC4); 为啥模板匹配用cv.imread的数据就没问题
 * @todo 参数校验
 */
function scaleSearch(src, templ, dst, mask, { similarity = 100, range, step, drawSrc } = { similarity: 100 }) {
    // We use the function: cv.matchTemplate (image, templ, result, method, mask = new cv.Mat());

    // Parameters
    // image	image where the search is running. It must be 8-bit or 32-bit floating-point.
    // templ	searched template. It must be not greater than the source image and have the same data type.
    // result	map of comparison results. It must be single-channel 32-bit floating-point.
    // method	parameter specifying the comparison method(see cv.TemplateMatchModes).
    // mask	mask of searched template. It must have the same datatype and size with templ. It is not set by default.
    // console.log(cv.Size);

    // -----循环缩放模板图片

    let size_res = null;
    let scale_times = 0; // 记录结果的缩放次序（从0开始）


    // 用于保存matchLoc，point ,将画框操作与scaleSearch解耦
    let match_rect_data_loc;
    let match_rect_data_point;

    let start_size = range[0];
    let target_size = range[1];

    let step_w = (step.w != undefined) ? Math.abs(step.w) : -1;
    let step_h = (step.h != undefined) ? Math.abs(step.h) : -1;
    if ((step_w == -1 && step_h == -1) || (step_w != -1 && step_h != -1)) {
        console.warn("参数 data.step异常");
        return;
    }

    // reduce or amplify
    let isReduce = (start_size.w > target_size.w);
    if (step_w != -1)
        step_w = isReduce ? (0 - step_w) : step_w;
    if (step_h != -1)
        step_h = isReduce ? (0 - step_h) : step_h;

    let curr_size = { w: start_size.w, h: start_size.h };
    function isEnd() {
        return (isReduce ? (curr_size.w <= target_size.w) : (curr_size.w >= target_size.w));
    }

    // for (let i = start_i; i < end_i; i++) {
    let i = 0;

    let iDiffNum_res = 100; // iDiffNum值越小，对应越相似的结果
    let similarity_res = 0;
    while (!isEnd()) {

        let templ_c = templ.clone();
        let width = target_size.w;
        let height = target_size.h; // 防止死循环
        if (step_w != -1) {
            width = curr_size.w + step_w;
            height = (width / templ.cols) * templ.rows;

        } else if (step_h != -1) {
            height = curr_size.h + step_h;
            width = (height / templ.rows) * templ.cols;
        }

        /** @todo cv.Size() 会自动向下取整 */
        // width = Math.floor(width);
        // height = Math.floor(height);

        if (width > src.cols || height > src.rows) {
            // continue 就行了，不是break,也有可能先 由大到小
            continue;// 模板图片尺寸不能比待搜索图片大
        }
        if (width == 0 || height == 0) {
            continue;
        }
        // console.log(width,height);
        // console.log(cv.resize);
        // console.log(cv.size);
        // console.log(new cv.Size(width, height));
        let size = new cv.Size(width, height);
        curr_size.w = width;
        curr_size.h = height;
        // let size = new cv.Size(Math.ceil(width), Math.ceil(height));

        // console.log(cv.INTER_AREA, cv.INTER_CUBIC, cv.INTER_LINEAR);
        if (isReduce) {
            cv.resize(templ_c, templ_c, size, 0, 0, cv.INTER_AREA);
        } else {
            cv.resize(templ_c, templ_c, size);// 默认 0,0,cv.INTER_LINEAR
            // cv.resize(templ_c, templ_c, size, 0, 0, cv.INTER_CUBIC);
        }


        try {
            // -----模板匹配
            // OpenCV.js 官方文档 模板匹配 https://docs.opencv.org/4.5.5/d8/dd1/tutorial_js_template_matching.html 
            // cv.matchTemplate(src, templ_c, dst, cv.TM_CCOEFF_NORMED, mask);
            cv.matchTemplate(src, templ_c, dst, 0); // 0: cv.TM_SQDIFF 4:cv.TM_CCOEFF 5:cv.TM_CCOEFF_NORMED
        } catch (err) {
            console.error("模板匹配出错error：", err);
        }

        let result = cv.minMaxLoc(dst, mask);
        // console.log(result);
        let maxLoc = result.maxLoc;
        let minLoc = result.minLoc;
        let minVal = result.minVal || -1;
        let maxVal = result.maxVal;
        let matchLoc;

        // let ROI = new cv.Rect(minLoc.x, maxLoc.y, width, height);

        let src_c = src.clone();// （图片二值化了，最后要显示的是原图）原图上标记
        matchLoc = minLoc;

        //-----根据result中最大值位置 画出矩形和中心点
        // let center = new cv.Point(minLoc.x + templ_c.cols / 2, minLoc.y + templ_c.rows / 2);

        // console.log(templ_c.cols, templ_c.rows);
        let point = new cv.Point(matchLoc.x + templ_c.cols, matchLoc.y + templ_c.rows);
        if (drawSrc) {
            cv.rectangle(drawSrc, matchLoc, point, new cv.Scalar(Math.floor(Math.random() * 256), Math.floor(Math.random() * 256), Math.floor(Math.random() * 256), 255), 2, cv.LINE_8, 0);
        }


        // console.log(cv.Rect);


        // let img_ROI = new cv.Rect(matchLoc, new cv.Point(matchLoc.x + templ_c.cols, matchLoc.y + templ_c.rows));// width,height undefined???
        // let img_ROI = new cv.Rect(matchLoc.x, matchLoc.y, templ_c.cols, templ_c.rows); // 直接这样写也得结果 但不知道会不会有问题
        let img_ROI = calcRect(matchLoc, point); // 模仿Rect_<_Tp>::Rect_
        let img = src.clone();
        // console.log(img, img_ROI);
        // let ROI_img = img(img_ROI); // img is not a function
        /***
         * Mat Mat::operator()( const Rect& roi ) const
         *   {
         *       return Mat(*this, roi);
         *   }
         *
         */
        // let ROI_img = new cv.Mat(img, img_ROI); // return Mat(*this, roi); js不支持
        let ROI_img = img.roi(img_ROI); // return Mat(*this, roi); 
        // console.log(ROI_img);
        // cv.imshow('screenshot_bag_marked_Canvas', ROI_img);

        // let match_rect = calcRect(matchLoc, point);
        // let ROI_img_original = src_c.roi(match_rect); // 比对的是二值化的图片，我们要的是原图(识别数字、显示识别区域有用)

        //-----进行相似度比较
        let iDiffNum = pHash(ROI_img, templ_c);
        // console.log(templ_c.cols, templ_c.rows, iDiffNum);

        let threshold_num = 100 - similarity;

        if (iDiffNum <= threshold_num) {
            if (iDiffNum == 0) {
                // 完全一致
                iDiffNum_res = 0;
                similarity_res = 100;
                scale_times = i;
                size_res = size;

                match_rect_data_loc = matchLoc;
                match_rect_data_point = point;
                // 完全一致，直接跳出循环
                break;
            } else {
                // 保存最小的那个数
                if (iDiffNum_res > iDiffNum) {
                    iDiffNum_res = iDiffNum;
                    similarity_res = 100 - iDiffNum;
                    scale_times = i;
                    size_res = size;
                    match_rect_data_loc = matchLoc;
                    match_rect_data_point = point;
                }
            }
        }

        templ_c.delete();
        src_c.delete();

        img.delete();
        ROI_img.delete();

        i++;

        // console.log("^^^");
    }

    return {
        // "templ_b_w": templ_b_w,// 模板（二值化）
        // "ROI_img_b_w": ROI_img_b_w,// 选区（二值化）

        // res,
        iDiffNum_res,
        similarity_res,// 结果相似度
        size_res,
        scale_times,// 记录结果匹配序次（从0开始）
        // ROI_img_s,
        match_rect_data_loc,
        match_rect_data_point
    };
}

/***
 * @function
 * @memberof window.$mocvf
 * @description 获取匹配得到的区域;  为解决 new cv.Rect(object,cv.Point)得到的rect ，其width,height 为undefined的问题
 * @param { Loc } pt1
 * @param { cv.Point } pt2
 * 
 * @returns { cv.Rect }
 * 
 * @example
 * // 原功能Rect_
 * Rect_<_Tp>::Rect_(const Point_<_Tp>& pt1, const Point_<_Tp>& pt2)
    {
        x = std::min(pt1.x, pt2.x);
        y = std::min(pt1.y, pt2.y);
        width = std::max(pt1.x, pt2.x) - x;
        height = std::max(pt1.y, pt2.y) - y;
    }
 * 
*/
function calcRect(pt1, pt2) {
    let x = pt1.x < pt2.x ? pt1.x : pt2.x;
    let y = pt1.y < pt2.y ? pt1.y : pt2.y;
    let w = pt1.x > pt2.x ? pt1.x : pt2.x - x;
    let h = pt1.y > pt2.y ? pt1.y : pt2.y - y;
    return new cv.Rect(x, y, w, h);
}

/***
 * @function
 * @memberof window.$mocvf
 * @description
 * @param { cc.Mat } matSrc1
 * @param { cc.Mat }  matSrc2
 * 
 * @returns { number } iDiffNum
 * 
 * @link opencv中的模板匹配-- 2.4 自适应目标匹配 https://blog.csdn.net/qq_46418503/article/details/119675943
 * 
*/
function pHash(matSrc1, matSrc2)
//int main()
{
    let matDst1 = new cv.Mat();
    let matDst2 = new cv.Mat(); // cv.resize 1,2参数类型检查 不能是undefined

    //    let matSrc1 = cv.imread("../1.jpg");
    //    let matSrc2 = cv.imread("../3.jpg");
    // console.log(new cv.Size(32, 32), cv.INTER_CUBIC);

    /** @todo 这边改成了模版尺寸(88x96)的 因数，不知道会不会有问题。写死封装不完善 */
    cv.resize(matSrc1, matDst1, new cv.Size(44, 48), 0, 0, cv.INTER_CUBIC);
    cv.resize(matSrc2, matDst2, new cv.Size(44, 48), 0, 0, cv.INTER_CUBIC);

    // console.log(cv.CV_BGR2GRAY,cv.COLOR_BGR2GRAY);// cv.CV_BGR2GRAY 废弃
    cv.cvtColor(matDst1, matDst1, cv.COLOR_BGR2GRAY);
    cv.cvtColor(matDst2, matDst2, cv.COLOR_BGR2GRAY);

    matDst1.convertTo(matDst1, cv.CV_32F);
    matDst2.convertTo(matDst2, cv.CV_32F);

    // opencv.js不支持 dct  
    // cv.dct(matDst1, matDst1);
    // cv.dct(matDst2, matDst2);

    // ！！！！这边跟原博文不一致，换成了dft
    // Is opencv.js supports dft and dct now? #22383 https://github.com/opencv/opencv/issues/22383
    // 官方 cv.dft https://docs.opencv.org/4.x/dd/d02/tutorial_js_fourier_transform.html
    // 图像处理中的傅里叶变换 https://www.cnblogs.com/tenderwx/p/5245859.html
    cv.dft(matDst1, matDst1);
    cv.dft(matDst2, matDst2);

    let iAvg1 = 0;
    let iAvg2 = 0;
    let arr1 = new Array(64);
    let arr2 = new Array(64);

    for (let i = 0; i < 8; i++) {
        // uchar * data1 = matDst1.ptr < uchar > (i);
        // uchar * data2 = matDst2.ptr < uchar > (i);

        // let data1 = matDst1.ptr(i);
        // let data2 = matDst2.ptr(i);

        let data1 = matDst1.ucharPtr(i);
        let data2 = matDst2.ucharPtr(i);

        // console.log(data1, data2);

        let tmp = i * 8;

        for (let j = 0; j < 8; j++) {
            let tmp1 = tmp + j;

            arr1[tmp1] = data1[j];
            arr2[tmp1] = data2[j];

            iAvg1 += arr1[tmp1];
            iAvg2 += arr2[tmp1];
        }
    }

    iAvg1 /= 64;
    iAvg2 /= 64;

    for (let i = 0; i < 64; i++) {
        arr1[i] = (arr1[i] >= iAvg1) ? 1 : 0;
        arr2[i] = (arr2[i] >= iAvg2) ? 1 : 0;
    }

    let iDiffNum = 0;

    for (let i = 0; i < 64; i++) {
        if (arr1[i] != arr2[i]) {
            ++iDiffNum;
        }
    }

    matDst1.delete();
    matDst2.delete();

    //    cout<<iDiffNum<<endl;
    return iDiffNum;
}

/**
 * @function
 * @memberof window.$mocvf
 * @description 图片二值化处理
 * @param { cv.Mat } src 要处理的图像
 * @param { number[] } arr_l lowerb
 * @param { number[] } arr_h upperb
 * @param { cv.Mat } dst 用于输出二值化图像
 * 
 * @returns { void }
 * @link OpenCV.js 官方文档 cvtColor inRange 使用示例 https://docs.opencv.org/4.5.5/db/d64/tutorial_js_colorspaces.html
 * @todo 数据转化方法用cv.imshow,cv.imread 怪怪的，但不转模板匹配函数会报错。有时间找到正确的方法，把它放到这个函数里，让out跟scr格式一致
 * 
*/
function bwImage(src, arr_l, arr_h, dst) {
    let src_original = src.clone(); // 待查询 ↑提高变量作用域
    let low_hsv_src = new cv.Mat(src_original.rows, src_original.cols, src_original.type(), arr_l);
    let high_hsv_src = new cv.Mat(src_original.rows, src_original.cols, src_original.type(), arr_h);
    // let src_b_w = new cv.Mat(); // 保存二值化图片blank&white
    // cv.inRange(src_original, low_hsv_src, high_hsv_src, src_b_w);
    cv.inRange(src_original, low_hsv_src, high_hsv_src, dst);

    src_original.delete();

}

/***
 * @function
 * @memberof window.$mocvf
 * @description 两个图像相似度比较。
 * @description 最后就是模板匹配了，匹配的方式有很多种，这里采用的是对数字和模板进行像素点的匹配，根据相同像素点的百分占比来得出结论，相同像素点越多，说明匹配相似度越高，最后以匹配程度最高的作为最终识别结果
 * @param { cv.Mat } img1 img1 注意保证两个图的通道数一致
 * @param { cv.Mat } img2 img2 注意保证两个图的通道数一致
 * 
 * @returns { float } [0,1]
 * @link https://blog.csdn.net/m0_71741835/article/details/127937354
 * 
*/
function mmCompare(img1, img2) {
    if (img1.channels() !== img2.channels()) {
        console.error("img1与img2通道数不一致");
        return 0;
    }
    // console.log(img1.channels(), img2.channels());
    let my_temp = new cv.Mat();
    if (img1.cols > img2.cols) {
        // console.log("xxx");
        cv.resize(img2, my_temp, img1.size());
    } else {
        // console.log("yyy");
        cv.resize(img1, my_temp, img2.size());
    }
    let rows, cols;
    let img_point, temp_point; //像素类型uchar

    // console.log(my_temp.rows, img1.rows, img2.rows);
    // console.log(my_temp.cols, img1.cols, img2.cols);
    // MatVector ptr https://docs.opencv.org/4.5.5/de/d06/tutorial_js_basic_ops.html
    rows = my_temp.rows;
    cols = my_temp.cols * img2.channels();

    let result, same = 0.0,
        different = 0.0;
    for (let i = 0; i < rows; i++) //遍历图像像素
    {
        //获取像素值
        if (img1.cols > img2.cols) {
            img_point = img1.ucharPtr(i);
        } else {
            img_point = img2.ucharPtr(i);
        }
        temp_point = my_temp.ucharPtr(i);
        for (let j = 0; j < cols; j++) {
            // let test1 = img_point[j];
            // let test2 = temp_point[j];
            // console.log(test1, test2);
            if (img_point[j] == temp_point[j]) {
                same++; //记录像素相同的个数
                // if (img_point[j] !== 0) {
                //     same++;//记录像素相同的个数
                // }
            } else {
                different++; //记录像素不同的个数
            }

        }
    }
    result = same / (same + different);
    my_temp.delete();
    return result; //返回匹配结果
}


